fix(runtime): collapse the published-seed read to the single env-wide read its gate produces - #15592
Conversation
… org rung Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…dentity Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
… read its gate produces `applyPublishedSeeds` read each just-published seed body twice when a session had an active organization — once naming the org, then env-wide. `seed` is `allowOrgOverride: false` and `getMetaItem` resolves `organizationIdForMetaRead` once at its top, so both rungs asked the engine the same predicates and served the same answer. Measured, not reasoned: an ablation neutering the second rung reddens nothing on a pinned publish-then-read path, with a positive control that the ladder is reached and a `view` control proving the comparison does separate the rungs for an org-overridable type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ed-read-dead-org-rung
…ble and pin its seams Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a6c91315653b8c8954c759980b7225298310d6f8 && git checkout a6c91315653b8c8954c759980b7225298310d6f8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ca46f8f128784987f13b3e89dc549e9f4203994e a400e6ed9e43d2b0d4b42d3e4efeb41cacedbc13 && git checkout -B drift-repro ca46f8f128784987f13b3e89dc549e9f4203994e && git merge --no-ff a400e6ed9e43d2b0d4b42d3e4efeb41cacedbc13
node scripts/docs-audit/affected-docs.mjs --json ca46f8f128784987f13b3e89dc549e9f4203994e
|
Fixes #15068
applyPublishedSeeds— the route-level seed apply behindPOST /packages/:id/publish-drafts— read each just-publishedseedbody twice whenever the session had an active organization: once naming the organization, then once env-wide. This collapses that ladder to the single read the gate already produces, and rewrites the comment to say the scope is decided by the registry flag, not by this call site.The fence this card came with, and how it was cleared
Triage was explicit that a registry reading alone was not licence to delete anything: the original comment says resolving the wrong scope "is what silently produced 0 rows loaded", so the ladder was written for a real outage. The deletion is licensed by measurement instead.
The pinned publish-then-read path (
packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts). The publish is real — astate:'draft'seed row promoted by the shippingpublishPackageDraftson a realObjectStackProtocolImplementation. The read-back is real — the same protocol instance servinggetMetaItemover the same engine, reading the row publish just wrote. Exactly one thing is doubled, and only to reach the code under test at all: the route-level apply runs only for protocols that do not self-apply seeds insidepublishPackageDrafts, so the second call presents apublishPackageDraftsthat reports the published seed without aseedAppliedfield — the population this fallback documents itself as existing for.The positive control (§0) — without it, "nothing reddened" is indistinguishable from "nothing ran":
activerow lands insys_metadata);sys_metadataand resolved the row publish had just written (item.object === 'project', two records back);publishPackageDraftswas handedorganizationId: 'org_acme', and oneresolveActiveOrganizationIdcall serves both it andapplyPublishedSeeds. That is what put the ladder on its two-rung branch.The ablation, direction predicted before running: §0/§1 stay green (the rung is dead) and §2/§3 flip green. The second rung was neutered on disk, proven by counting both the removed text (1 → 0) and an injected
OS_ABLATION_15068marker (0 → 1) plus a non-emptygit diff --stat, under atrap … EXIT INT TERMrestoring through an absolute path.packages.tsgit checkout HEAD -- ABSOLUTE_PATH1c0349a734d…== worktreegit hash-object(both non-empty),git diff HEADempty, marker count back to 0, removed text back to 1⇒ Neutering the rung reddened nothing on a path proven to reach it. The rung is dead.
Resolution path, stated because a source-only mutation can measure nothing. The mutated file is reached by relative source (
./http-dispatcher.js→./domains/packages.js), so nodist/hop is involved for it andablation-dist-preflightdoes not apply — and the ablation's own §2/§3 flip is the proof the edit reached the running code. The gate under test (@objectstack/metadata-protocol,@objectstack/metadata-core) resolves throughexportstodist/, so the dependency closure was built before any leg was read (pnpm --filter '@objectstack/runtime^...' build, exit 0).The mechanism, and the case analysis
getMetaItemopens withorganizationIdForMetaRead(request.type, request.organizationId)(#14908) and spends that binding — never the raw argument — on every read beneath it;request.organizationIdappears exactly once in the method.seeddeclaresallowOrgOverride: false, so the predicate answersundefinedwhatever organization arrives.§1 measures that directly rather than restating it: both rungs, run against one store, produce byte-identical engine predicates and byte-identical answers on the hit branch and the miss branch. The control that makes it a reading rather than a tautology: the same comparison on
view(allowOrgOverride: true) does separate the two rungs, and the org-first one names a partition the env-wide one never touches.⭐ Measured here and not claimed by the card:
getMetaItemanswers a wrapper with noitem— not a falsy value — for a name it cannot resolve. Soif (item) breakfires on the first attempt even on a miss, and the only branch on which the second rung ever executed is the throw branch, where it repeated the identical failing read.The one observable that changes
On the throw branch, the duplicated rung appended the same sentence to the client-facing
seedApplied.errors[]twice — a payload an author reads as two distinct failures. A failed read-back is now reported once (§3, asserting the ADR-0112 declaration oncodeandstatus; never a baretoThrow(), since this door reports rather than throws). Nothing about which row a publish resolves, or whether its rows load, moves.Clause ② —
noThe two attempts cannot differ, so removing one moves no contract: no export changes, no accepted key or value is added, and
applyPublishedSeedsis module-private. The bump argument against the Check Changeset rule text ("a purely additive widening … takes at leastminor"): this is a removal that widens nothing, sopatchis what the act requires, and thefixcommit type does not raise it.The two sentences in this file now agree
packages/runtime/src/domains/packages.ts:692(theappflip, repaired by #15063) and the new sentence say the same thing: the predicate answersundefinedfor every non-overridable type, the read is env-wide by construction, and ⛔ dropping the organization is the repair, not an oversight to undo. One narrowing worth recording against the dispatch's reading::692states that of the pluralgetMetaItemsonapp, citing #14683 — not ofgetMetaItemonseed. The file's two sentences were about different verbs, so a reader could believe both were true of their own; they now agree on both verbs.The class is closed in this package, re-verified with the card's own method: every non-test metadata read call site under
':(glob)packages/runtime/src/**/*.ts'gives 9 (positive control: 18 with test files included), and exactly two comment lines under that pathspec stated the pre-gate rule — both at this site. #14771 / #15063 repaired the other member.One collateral edit, declared
packages/metadata-protocol/src/protocol.ts— the read gate's own docblock enumerates the runtime callers that hand it a raw active organization and counts them: "FOUR, across two files", withapplyPublishedSeedsas one bullet. This change makes that count and that bullet false, so leaving them would author a new member of the very class this card closes. The enumeration now reads THREE, all in one file, with the fourth recorded as having moved into the "names no organization at all" bucket and why. Comment-only, no behaviour, no changeset entry (it publishes nothing). Serial re-measured for this second file: 35/35 open PR heads fetched per-ref at--depth=80with a computed merge-base on every row — 0 hits onpackages/runtime/src/domains/packages.ts, one hit onprotocol.ts(#15395) whose hunks are at lines 78/1736/10571+ and do not reach this region.Verification — all on
a400e6ed9e4, the final commitpnpm --filter @objectstack/runtime --filter @objectstack/metadata-protocol test— runtime 3253 passed / 227 files; metadata-protocol 2379 passed, 10 skipped / 164 files.pnpm --filter @objectstack/runtime --filter @objectstack/metadata-protocol typecheck— exit 0. Both script names echoed (a zero-match--filterexits 0 having run nothing). The new test file is really in the program:tsc -p tsconfig.test.json --listFilesnames it, andcheck:test-typecheckreports the ledger held.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackon a tree the tool does not call stale: 59 families, 59 green. Two were re-run after the full workspace build cleared theirPREREQUISITE NOT MET(check:dual-build-cjs-loads,check:type-check-debt— exit 3, recorded as NOT MEASURED, never as a pass). Two were genuinely red on the first sweep and are fixed here:check:engine-double-contract(the new double needed its ledger seams,--write) andcheck:objectql-double-limit(thefinddouble was limit-blind; it now applies the caller's bound by presence). Re-deriving after the ledger file joined the changed set added 6 more families — all run, all green.3read from--format json, population read from eslint's own--print-configrather than guessed. The invariance that makes the narrowing a measurement: this repo runs oneeslint.config.mjswhich never enables type-aware linting for any file (eslint.config.mjs:327-329, with its own recorded positive control), so this diff cannot move the verdict on a file it did not touch.Not done, with reasons
_packageId/_diagnosticssurvive the unwrap andSeedLoaderRequestSchemarejects them asunrecognized_keys#15591: with the shipping protocol behind this fallback, the read-back envelope carries_packageIdand_diagnostics(added bydecorateMetadataItem), andSeedLoaderRequestSchemarejects them asunrecognized_keys— soapplyPublishedSeedsanswers[invalid_metadata] … seeds.0 [unrecognized_keys]instead of loading rows. Measured on this branch. A different defect class (provenance keys surviving the unwrap, not org scope), so it is not fixed here.Generated by Claude Code